﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using iTool.Cloud.DataSearch.DirectoryProvider;
using iTool.ClusterComponent;

using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Index.Extensions;
using Lucene.Net.Spatial;
using Lucene.Net.Spatial.Prefix;
using Lucene.Net.Spatial.Prefix.Tree;
using Lucene.Net.Util;

using Microsoft.Data.Sqlite;

using Orleans;

using Spatial4n.Context;
using Spatial4n.Shapes;

namespace iTool.Cloud.DataSearch.ServiceProvider
{
    public class IndexManageService : iToolServiceStorageBase<List<string>>, IIndexManageService
    {
        bool isLocal = false;
        List<string> _State;
        List<string> _LocationState;
        //List<(string field,string type)> SortFields = new List<(string field, string type)>();
        List<string> ProxyState
        {
            get
            {
                return isLocal ? _State : State;
            }
            set
            {
                if (isLocal)
                {
                    _State = value;
                }
                else
                {
                    State = value;
                }
            }
        }

        // AUTOINCREMENT 自增
        const string PRIMARY_KEY_KEYWORD = "FIELDNAME";
        const string VALUE_NOT_NULL = "NOT NULL";
        const string PRIMARY_KEY_KEYWORD_TYPE = "VARCHAR PRIMARY KEY";
        const string CREATE_FIELD_SQL = "{0} {1} {2}";

        const string DirectoryPath = "./Storage";
        const string DataSource = DirectoryPath + "/DataBase.master";

        const string IndexDirectoryPath = "./Storage/Index";
        string IndexDataSource = IndexDirectoryPath + "/{0}.index";
        string indexConnectionString, connectionString, tableName;
        const string DataSourceVersion = DirectoryPath + "/Versions/{0}.version";
        protected string SqliteDBVersionConnectionInstanceOfPrivate { get; set; } = String.Empty;


        IndexWriter indexWriter;

        public IndexManageService()
        {
            if (!Directory.Exists(IndexDirectoryPath))
                Directory.CreateDirectory(IndexDirectoryPath);
        }

        public async override Task OnActivateAsync()
        {
            this.tableName = this.GetStringKey();
            this.indexConnectionString = new SqliteConnectionStringBuilder
            {
                DataSource = String.Format(this.IndexDataSource, JenkinsHash.ComputeHash(tableName)),
                Mode = SqliteOpenMode.ReadWriteCreate,
                Cache = SqliteCacheMode.Shared,
                Pooling = true
            }.ToString();

            this.connectionString = new SqliteConnectionStringBuilder
            {
                DataSource = DataSource,
                Mode = SqliteOpenMode.ReadOnly,
                Cache = SqliteCacheMode.Shared,
                Pooling = true
            }.ToString();

            string sql = @$"CREATE TABLE IF NOT EXISTS FieldsMetaData({string.Format(CREATE_FIELD_SQL, PRIMARY_KEY_KEYWORD, PRIMARY_KEY_KEYWORD_TYPE, VALUE_NOT_NULL)})";

            using (var connection = new SqliteConnection(this.indexConnectionString))
            {
                connection.Open();
                using (SqliteCommand command = new SqliteCommand(sql, connection))
                {
                    command.ExecuteNonQuery();
                }
            }

            Lucene.Net.Store.Directory directory = new iToolCloudDirectory(this.tableName, true);
            Analyzer analyzer = new JieBaAnalyzer(TokenizerMode.Search);
            IndexWriterConfig indexWriterConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer);
            indexWriterConfig.SetMaxBufferedDocs(1000);
            indexWriter = new IndexWriter(directory, indexWriterConfig);

            await base.OnActivateAsync();
        }

        public async Task AddIndexAsync(params long[] ids)
        {
            if (!ids.Any() || !this.IsAnyIndex())
            {
                return;
            }

            var fields = this.GetIndexFields();

            await using (var connection = new SqliteConnection(this.connectionString))
            {
                await connection.OpenAsync();
                await using (SqliteCommand command = new SqliteCommand($"select _IDX,{string.Join(',', fields)} from {this.tableName} where _IDX in ({string.Join(',', ids)})", connection))
                {
                    var reader = await command.ExecuteReaderAsync();
                    if (reader.HasRows)
                    {
                        var documents = this.GetDocumentsByDataReader(reader, fields);
                        this.indexWriter.AddDocuments(documents);
                    }
                }
            }
        }

        public async Task ModifyIndexAsync(params long[] ids)
        {
            if (!ids.Any() || !this.IsAnyIndex())
            {
                return;
            }

            var fields = this.GetIndexFields();

            await using (var connection = new SqliteConnection(this.connectionString))
            {
                await connection.OpenAsync();
                await using (SqliteCommand command = new SqliteCommand($"select _IDX,{string.Join(',', fields)} from {this.tableName} where _IDX in ({string.Join(',', ids)})", connection))
                {
                    await using (var reader = await command.ExecuteReaderAsync())
                    {
                        if (reader.HasRows)
                        {
                            var documents = this.GetDocumentsByDataReader(reader, fields);
                            foreach (var doc in documents)
                            {
                                string idValue = doc.GetField("id").GetStringValue();
                                this.indexWriter.UpdateDocument(new Term("id", idValue), doc);
                            }
                        }
                    }
                }
            }
        }

        public async Task ReBuildAllAsync()
        {
            if (!this.IsAnyIndex())
                return;

            // 删除所有索引
            await this.RemoveAllAsync();

            var fields = this.GetIndexFields();

            await using (var connection = new SqliteConnection(this.connectionString))
            {
                await connection.OpenAsync();

                await using (SqliteCommand command = new SqliteCommand($"select _IDX,{string.Join(',', fields)} from {this.tableName}", connection))
                {
                    var reader = await command.ExecuteReaderAsync();

                    if (reader.HasRows)
                    {
                        var documents = this.GetDocumentsByDataReader(reader, fields);
                        this.indexWriter.AddDocuments(documents);
                    }
                }
            }
        }

        public Task RemoveAllAsync()
        {
            if (!this.IsAnyIndex())
                return Task.CompletedTask;

            this.indexWriter.DeleteAll();
            Document doc = new Document();
            var int64Field = new Field("id", "0", new FieldType
            {
                IsStored = true,
                IsIndexed = true,
                IsTokenized = false
            });
            doc.Add(int64Field);
            this.indexWriter.AddDocument(doc);
            return Task.CompletedTask;
        }

        public Task RemoveIndexAsync(params long[] ids)
        {
            if (!this.IsAnyIndex())
                return Task.CompletedTask;
            //PhraseQuery builder = new PhraseQuery();
            foreach (var id in ids)
            {
                //var int64Field = new Field("id", id.ToString(), new FieldType
                //{
                //    IsStored = true,
                //    IsIndexed = true,
                //    IsTokenized = false
                //});
                this.indexWriter.DeleteDocuments(new Term("id", id.ToString()));
            }
            //this.indexWriter.DeleteDocuments(builder);
            return Task.CompletedTask;
        }

        public async Task<bool> IsNeedReBuildIndexByFieldsAsync(List<string> locations, params string[] fields)
        {
            if (!fields.Any() && !locations.Any())
            {
                return false;
            }

            if (fields.Any())
            {
                fields = fields.Select(item => item.ToUpper()).Distinct().ToArray();
            }

            if (locations.Any())
            {
                locations = locations.Select(item => item.ToUpper()).Distinct().ToList();
            }

            bool isNeedWrite = false;

            // 如果不存在坐标字段，直接赋值
            if (!this._LocationState.Any())
            {
                isNeedWrite = true;
                this._LocationState = locations;
            }

            // 如果不存在索引字段，直接赋值
            if (!this.ProxyState.Any())
            {
                isNeedWrite = true;
                this.ProxyState = fields.ToList();
            }


            // 处理索引字段
            foreach (var item in fields)
            {
                if (!this.ProxyState.Any(fName => fName == item))
                {
                    isNeedWrite = true;
                    this.ProxyState = this.ProxyState.Concat(fields).Distinct().ToList();
                    break;
                }
            }
            // 处理地图坐标
            foreach (var item in locations)
            {
                if (!this._LocationState.Any(fName => fName == item))
                {
                    isNeedWrite = true;
                    this._LocationState = this._LocationState.Concat(locations).Distinct().ToList();
                    break;
                }
            }

            if (isNeedWrite)
            {
                await this.WriteStateAsync();
            }

            return isNeedWrite;
        }

        protected async override Task ClearStateAsync()
        {
            await using (var connection = new SqliteConnection(this.indexConnectionString))
            {
                await connection.OpenAsync();
                var command = new SqliteCommand($"DELETE FROM FieldsMetaData", connection);
                await command.ExecuteNonQueryAsync();
            }
        }

        protected override Task ReadStateAsync()
        {
            this.ProxyState = new List<string>();
            this._LocationState = new List<string>();
            using (var connection = new SqliteConnection(this.indexConnectionString))
            {
                connection.Open();
                var command = new SqliteCommand($"SELECT {PRIMARY_KEY_KEYWORD},rowid FROM FieldsMetaData", connection);
                var reader = command.ExecuteReader();

                if (!reader.HasRows)
                {
                    return Task.CompletedTask;
                }

                while (reader.Read())
                {
                    string field = reader.GetString(0);
                    if (field.StartsWith("$sort&"))
                    {
                        string[] fields = field.Split('&');
                        if (fields.Length == 3)
                        {
                            this._LocationState.Add(string.Format("{0}&{1}", fields[1], fields[2]));
                        }
                        else
                        {
                            this.ProxyState.Add(field);
                        }
                    }
                    else
                    {
                        this.ProxyState.Add(field);
                    }
                }
            }
            return Task.CompletedTask;
        }

        protected async override Task WriteStateAsync()
        {
            if (this.ProxyState.Any() || this._LocationState.Any())
            {
                //var sqls = (this.ProxyState.Concat(this.SortFields.Select(item => string.Format("{0}_{1}_sort", item.field, item.type)))).Select(item => $"INSERT INTO FieldsMetaData({PRIMARY_KEY_KEYWORD}) VALUES('{item}')");
                
                var sqls = this.ProxyState.Concat(this._LocationState.Select(item => string.Format("{0}&{1}", "$sort", item))).Select(item => $"INSERT INTO FieldsMetaData({PRIMARY_KEY_KEYWORD}) VALUES('{item}')");
                await using (var connection = new SqliteConnection(this.indexConnectionString))
                {
                    await connection.OpenAsync();
                    var command = new SqliteCommand("DELETE FROM FieldsMetaData;" + string.Join(';', sqls), connection);
                    var reader = await command.ExecuteReaderAsync();
                }
            }
            else
            {
                await this.ClearStateAsync();
            }
        }

        public async void OnActivateAsync(long hash, string name)
        {
            this.isLocal = true;
            this.tableName = name;
            this.indexConnectionString = new SqliteConnectionStringBuilder
            {
                DataSource = String.Format(this.IndexDataSource, JenkinsHash.ComputeHash(this.tableName)),
                Mode = SqliteOpenMode.ReadWriteCreate,
                Cache = SqliteCacheMode.Shared,
                Pooling = true
            }.ToString();

            this.connectionString = new SqliteConnectionStringBuilder
            {
                DataSource = DataSource,
                Mode = SqliteOpenMode.ReadOnly,
                Cache = SqliteCacheMode.Shared,
                Pooling = true
            }.ToString();

            string sql = @$"CREATE TABLE IF NOT EXISTS FieldsMetaData({string.Format(CREATE_FIELD_SQL, PRIMARY_KEY_KEYWORD, PRIMARY_KEY_KEYWORD_TYPE, VALUE_NOT_NULL)})";

            using (var connection = new SqliteConnection(this.indexConnectionString))
            {
                connection.Open();
                using (SqliteCommand command = new SqliteCommand(sql, connection))
                {
                    command.ExecuteNonQuery();
                }
            }

            Lucene.Net.Store.Directory directory = new iToolCloudDirectory(this.tableName, true);
            Analyzer analyzer = new JieBaAnalyzer(TokenizerMode.Search);
            IndexWriterConfig indexWriterConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer);
            indexWriterConfig.SetMaxBufferedDocs(1000);
            indexWriter = new IndexWriter(directory, indexWriterConfig);
            if (indexWriter.NumDocs == 0)
            {
                Document doc = new Document();
                var int64Field = new Field("id", "0", new FieldType
                {
                    IsStored = true,
                    IsIndexed = true,
                    IsTokenized = false
                });
                doc.Add(int64Field);
                this.indexWriter.AddDocument(doc);
                this.indexWriter.Commit();
            }
            this.indexWriter.Commit();

            base.OnActivateAsync().Wait();
        }

        public async Task FlushAsync()
        {
            await this.CommitAsync();
        }

        public Task CommitAsync()
        {
            //this.indexWriter.Flush(false, true);
            this.indexWriter.Commit();
            return Task.CompletedTask;
        }

        public async Task RollbackAsync()
        {
            this.indexWriter.Rollback();
            this.indexWriter.Dispose();
            await Task.Delay(50);
            Lucene.Net.Store.Directory directory = new iToolCloudDirectory(this.tableName, true);
            Analyzer analyzer = new JieBaAnalyzer(TokenizerMode.Search);
            IndexWriterConfig indexWriterConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer);
            indexWriterConfig.SetMaxBufferedDocs(1000);
            indexWriter = new IndexWriter(directory, indexWriterConfig);

            //return Task.CompletedTask;
        }

        public Task<bool> IsHasIndexAsync(params string[] fields)
        {
            if (fields.Any())
            {
                if (this.ProxyState.Any())
                {
                    foreach (var field in this.ProxyState)
                    {
                        if (fields.Any(item => item == field))
                        {
                            return Task.FromResult(true);
                        }
                    }
                }
                return Task.FromResult(false);
            }

            return Task.FromResult(this.ProxyState.Any());
        }

        protected async Task<SqliteConnection> GetDBVersionConnectionAsync()
        {
            SqliteConnection result;

            if (string.IsNullOrWhiteSpace(this.SqliteDBVersionConnectionInstanceOfPrivate))
            {
                this.SqliteDBVersionConnectionInstanceOfPrivate = new SqliteConnectionStringBuilder
                {
                    DataSource = String.Format(DataSourceVersion, this.tableName),
                    //Mode = SqliteOpenMode.ReadWriteCreate,
                    Cache = SqliteCacheMode.Private,
                    Pooling = true
                }.ToString();
            }

            result = new SqliteConnection(this.SqliteDBVersionConnectionInstanceOfPrivate);

            if (result.State != System.Data.ConnectionState.Open)
            {
                await result.OpenAsync();
            }

            return result;
        }

        private bool IsAnyIndex() 
        {
            return this.ProxyState.Any() || this._LocationState.Any();
        }

        protected List<Document> GetDocumentsByDataReader(SqliteDataReader reader, List<string> fields) 
        {
            List<Document> documents = new List<Document>();

            var structValueFieldType = new FieldType
            {
                IsStored = true,
                IsIndexed = true,
                IsTokenized = false
            };

            while (reader.Read())
            {
                structValueFieldType.IsStored = true;
                Document doc = new Document();
                var int64Field = new Field("id", reader.GetInt64(0).ToString(), structValueFieldType);
                doc.Add(int64Field);

                int locationFieldsLength = this._LocationState.Count() * 2;
                int locationEndOffset = fields.Count - locationFieldsLength;

                for (int i = fields.Count; i > 0; i--)
                {
                    // 索引分为 坐标索引， 和普通索引。 倒排查询 可能会好做
                    if (this._LocationState.Any() && locationEndOffset < i)
                    {
                        // write 坐标
                        if (double.TryParse(reader[i].ToString(), out double y) && double.TryParse(reader[i - 1].ToString(), out double x))
                        {
                            IShape shape = SpatialContext.Geo.MakePoint(x, y);
                            SpatialStrategy strategy = new RecursivePrefixTreeStrategy(new GeohashPrefixTree(SpatialContext.Geo, 11), string.Format("{0}_{1}", fields[i - 2], fields[i-1]));
                            Field[] mapFields = strategy.CreateIndexableFields(shape);
                            foreach (Field field in mapFields)
                            {
                                doc.Add(field);
                            }
                        }
                        i--;
                        continue;
                    }

                    string value = reader[i].ToString();

                    if (DateTime.TryParse(value, out DateTime dateTime))
                    {
                        structValueFieldType.IsStored = false;
                        //value = dateTime.ToString("s");
                        //this.SortFields.Add((this.ProxyState[i], typeof(DateTime).Name));
                        doc.Add(new Field(fields[i - 1], dateTime.Ticks.ToString(), structValueFieldType));
                        //doc.Add(new Field(fields[i], value, structValueFieldType));
                    }
                    else if (double.TryParse(value, out double douValue))
                    {
                        structValueFieldType.IsStored = false;
                        //this.SortFields.Add((this.ProxyState[i], typeof(double).Name));
                        doc.Add(new Field(fields[i-1], value, structValueFieldType));
                    }
                    else
                    {
                        doc.Add(new TextField(fields[i - 1], value, Field.Store.NO));
                    }
                }
                documents.Add(doc);
            }

            return documents;
        }

        protected List<string> GetIndexFields()
        {
            var fields = new List<string>();
            if (this.ProxyState.Any())
            {
                fields = fields.Concat(this.ProxyState).ToList();
            }

            if (this._LocationState.Any())
            {
                foreach (var item in this._LocationState)
                {
                    var fieldArray = item.Split('&');
                    int index = 0;
                    if (fields.Contains(fieldArray[index]))
                    {
                        fields.Remove(fieldArray[index]);
                    }
                    fields.Add(fieldArray[index]);
                    index++;
                    if (fields.Contains(fieldArray[index]))
                    {
                        fields.Remove(fieldArray[index]);
                    }
                    fields.Add(fieldArray[index]);
                }
            }
            return fields;
        }

        public Task<(List<string> locations, List<string> fields)> GetAllIndexFieldsAsync()
        {
            return Task.FromResult<(List<string> locations, List<string> fields)>((this._LocationState,this.ProxyState));
        }

        //protected async Task InitIndexAsync()
        //{
        //    using (var connection = await this.GetDBVersionConnectionAsync())
        //    {
        //        string sql = $"update {this.tableName} set isCreateIndex = 1";
        //        using (var command = new SqliteCommand(sql, connection))
        //        {
        //            await command.ExecuteNonQueryAsync();
        //        }
        //    }
        //}
    }
}
